﻿/////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2009 Eric Bowen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 
// associated documentation files (the "Software"), to deal in the Software without restriction, including 
// without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
// copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 
// following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial 
// portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 
// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Author's blog: http://blog.scrappydog.com
// Updated source code available:  http://scrappydb.codeplex.com
/////////////////////////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;
using System.Web.Profile;
using NLog;
using ScrappyDB.BaseClasses;
using ScrappyDB.Utilities;

namespace ScrappyDB.Membership
{
    public sealed class SdbProfileProvider : ProfileProvider
    {
        private static readonly Logger Log = LogManager.GetCurrentClassLogger();

        private string _applicationName;
        private MembershipCommon _common;
        private Db _db;

        public override string ApplicationName
        {
            get { return _applicationName; }
            set { _applicationName = value; }
        }

        #region  ******************* Overides To System.Web.Profile.ProfileProvider Methods *******************

        public override int DeleteInactiveProfiles(
            ProfileAuthenticationOption authenticationOption,
            DateTime userInactiveSinceDate)
        {
            throw new NotImplementedException();
        }

        public override int DeleteProfiles(ProfileInfoCollection profiles)
        {
            int deleteCount = 0;

            foreach (object profile in profiles)
            {
                var info = (ProfileInfo) profile;
                DeleteProfile(info.UserName);
                deleteCount++;
            }

            return deleteCount;
        }

        public override int DeleteProfiles(string[] usernames)
        {
            int deleteCount = 0;

            foreach (string user in usernames)
            {
                DeleteProfile(user);
                deleteCount++;
            }

            return deleteCount;
        }

        public override ProfileInfoCollection FindInactiveProfilesByUserName(
            ProfileAuthenticationOption authenticationOption,
            string usernameToMatch,
            DateTime userInactiveSinceDate,
            int pageIndex,
            int pageSize,
            out int totalRecords)
        {
            CheckParameters(pageIndex, pageSize);

            return GetProfileInfo(authenticationOption, usernameToMatch, userInactiveSinceDate,
                                  pageIndex, pageSize, out totalRecords);
        }

        public override ProfileInfoCollection FindProfilesByUserName(
            ProfileAuthenticationOption authenticationOption,
            string usernameToMatch,
            int pageIndex,
            int pageSize,
            out int totalRecords)
        {
            CheckParameters(pageIndex, pageSize);

            return GetProfileInfo(authenticationOption, usernameToMatch,
                                  null, pageIndex, pageSize, out totalRecords);
        }

        public override ProfileInfoCollection GetAllInactiveProfiles(
            ProfileAuthenticationOption authenticationOption,
            DateTime userInactiveSinceDate,
            int pageIndex,
            int pageSize,
            out int totalRecords)
        {
            CheckParameters(pageIndex, pageSize);

            return GetProfileInfo(authenticationOption, null, userInactiveSinceDate,
                                  pageIndex, pageSize, out totalRecords);
        }

        public override ProfileInfoCollection GetAllProfiles(
            ProfileAuthenticationOption authenticationOption,
            int pageIndex,
            int pageSize,
            out int totalRecords)
        {
            CheckParameters(pageIndex, pageSize);

            return GetProfileInfo(authenticationOption, null, null,
                                  pageIndex, pageSize, out totalRecords);
        }

        public override int GetNumberOfInactiveProfiles(
            ProfileAuthenticationOption authenticationOption,
            DateTime userInactiveSinceDate)
        {
            int inactiveProfiles;

            ProfileInfoCollection profiles =
                GetProfileInfo(authenticationOption, null, userInactiveSinceDate,
                               0, 0, out inactiveProfiles);

            return inactiveProfiles;
        }

        public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context,
                                                                          SettingsPropertyCollection ppc)
        {
            var username = (string) context["UserName"];
            //lookup userId
            Guid userId = _common.GetUserId(username);

            var isAuthenticated = (bool) context["IsAuthenticated"];

            // The serializeAs attribute is ignored in this provider implementation.

            var svc =
                new SettingsPropertyValueCollection();

            foreach (SettingsProperty prop in ppc)
            {
                var pv = new SettingsPropertyValue(prop);


                pv = GetPropertyValue(prop, userId, isAuthenticated);


                svc.Add(pv);
            }

            UpdateActivityDates(username, isAuthenticated);

            return svc;
        }

        public override void SetPropertyValues(SettingsContext context,
                                               SettingsPropertyValueCollection ppvc)
        {
            var username = (string) context["UserName"];
            //lookup userId
            Guid userId = _common.GetUserId(username);

            var isAuthenticated = (bool) context["IsAuthenticated"];
            Guid uniqueId = GetProfileId(username, isAuthenticated, false);
            if (uniqueId == Guid.Empty)
                CreateProfileForUser(username, isAuthenticated);

            foreach (SettingsPropertyValue pv in ppvc)
            {
                SetPropertyValue(pv, userId, isAuthenticated);
            }

            UpdateActivityDates(username, isAuthenticated);
        }

        #endregion

        #region  ******************* Private Methods *******************

        private static void CheckParameters(int pageIndex, int pageSize)
        {
            if (pageIndex < 0)
                throw new ArgumentException("Page index must 0 or greater.");
            if (pageSize < 1)
                throw new ArgumentException("Page size must be greater than 0.");
        }

        /// <summary>
        /// If no user currently exists in the database, a user record is created during the call to the GetProfileId private method.
        /// </summary>
        /// <param name="username"></param>
        /// <param name="isAuthenticated"></param>
        /// <returns></returns>
        private UserProfile CreateProfileForUser(string username, bool isAuthenticated)
        {
            //// Check for valid user name.

            if (username == null)
                throw new ArgumentNullException(username, "User name cannot be null.");
            if (username.Length > 255)
                throw new ArgumentException("User name exceeds 255 characters.", username);
            if (username.Contains(","))
                throw new ArgumentException("User name cannot contain a comma.", username);

            var profile = new UserProfile
                              {
                                  UserProfileId = Guid.NewGuid(),
                                  ApplicationName = ApplicationName,
                                  IsAnonymous = !isAuthenticated,
                                  LastActivityDate = DateTime.Now.ToUniversalTime(),
                                  LastUpdatedDate = DateTime.Now.ToUniversalTime(),
                                  UserName = username
                              };

            _db.SaveChanges(profile);

            return profile;
        }

        /// <summary>
        /// Deletes profile data from the database for the specified user name.
        /// </summary>
        /// <param name="username"></param>
        private void DeleteProfile(string username)
        {
            Guid userId = _common.GetUserId(username);

            if (userId == Guid.Empty)
                throw new SdbProfileProviderException(string.Format("Invalid username: {0}", username));

            SdbCollection<UserProfileProperty> properties =
                _db.Query<UserProfileProperty>(" UserId = '" + userId + "' ");
            foreach (UserProfileProperty property in properties)
            {
                _db.Delete(property);
            }

            UserProfile profile = GetProfile(username, false);
            _db.Delete(profile);
        }

        private UserProfile GetProfile(string username, bool isAuthenticated)
        {
            SdbCollection<UserProfile> profiles = _db.Query<UserProfile>(" UserName = '" + username + "' ");

            if (profiles == null || profiles.Count == 0)
            {
                return CreateProfileForUser(username, isAuthenticated);
                //profiles = _db.Query<UserProfile>(" UserName = '" + username + "' ");

                //if (profiles == null || profiles.Count == 0)
                //    throw new SdbProfileProviderException("User: '" + username + "' not found");
            }

            return profiles[0];
        }

        private Guid GetProfileId(string username, bool isAuthenticated, bool ignoreAuthenticationType)
        {
            UserProfile profile = GetProfile(username, isAuthenticated);
            return profile.UserProfileId;
        }

        //
        // GetProfileInfo
        // Retrieves a count of profiles and creates a 
        // ProfileInfoCollection from the profile data in the 
        // database. Called by GetAllProfiles, GetAllInactiveProfiles,
        // FindProfilesByUserName, FindInactiveProfilesByUserName, 
        // and GetNumberOfInactiveProfiles.
        // Specifying a pageIndex of 0 retrieves a count of the results only.
        //
        private ProfileInfoCollection GetProfileInfo(
            ProfileAuthenticationOption authenticationOption,
            string usernameToMatch,
            object userInactiveSinceDate,
            int pageIndex,
            int pageSize,
            out int totalRecords)
        {
            string sql = " ApplicationName = '" + ApplicationName + "' ";

            if (!string.IsNullOrEmpty(usernameToMatch))
                sql += " and UserName like '" + usernameToMatch + "%' ";

            if (userInactiveSinceDate != null)
                sql += " and LastActivityDate <= '" +
                       DbDateTime.ConvertDateTimeToIso8601((DateTime) userInactiveSinceDate) + "%' ";

            switch (authenticationOption)
            {
                case ProfileAuthenticationOption.Anonymous:
                    sql += " and IsAnonymous = 'True' ";
                    break;
                case ProfileAuthenticationOption.Authenticated:
                    sql += " and IsAnonymous = 'False' ";
                    break;
                default:
                    break;
            }

            totalRecords = _db.Count<UserProfile>(sql);

            int startPosition = pageIndex*pageSize;
            SdbCollection<UserProfile> profiles = _db.Query<UserProfile>(sql, null, startPosition, pageSize);

            var result = new ProfileInfoCollection();

            foreach (UserProfile profile in profiles)
            {
                result.Add(new ProfileInfo(profile.UserName, profile.IsAnonymous, profile.LastActivityDate,
                                           profile.LastUpdatedDate, 0));
            }

            return result;
        }

        private SettingsPropertyValue GetPropertyValue(SettingsProperty prop, Guid userId, bool isAuthenticated)
        {
            var result = new SettingsPropertyValue(prop);

            //we are going to get ALL property values for this user and cache them instead of making individual calls for each property
            SdbCollection<UserProfileProperty> p =
                _db.Query<UserProfileProperty>(string.Format(" UserId = '{0}'", userId));

            if (p != null && p.Count > 0)
            {
                foreach (UserProfileProperty property in p)
                {
                    if (property.PropertyName == prop.Name)
                    {
                        result.PropertyValue = property.PropertyValue;
                        break;
                    }
                }
            }

            return result;
        }

        private void SetPropertyValue(SettingsPropertyValue pv, Guid userId, bool isAuthenticated)
        {
            SdbCollection<UserProfileProperty> p =
                _db.Query<UserProfileProperty>(" PropertyName = '" + pv.Name + "' and  UserId = '" + userId + "' ");

            if (p == null)
                p = new SdbCollection<UserProfileProperty>();

            UserProfileProperty v;

            if (p.Count == 0)
            {
                v = new UserProfileProperty
                        {
                            PropertyName = pv.Name,
                            UserId = userId,
                        };
                p.Add(v);
            }
            else
            {
                v = p[0];
            }

            if (v.ProfilePropertyId == Guid.Empty)
                v.ProfilePropertyId = Guid.NewGuid();

            v.PropertyValue = pv.PropertyValue == null ? string.Empty : pv.PropertyValue.ToString();

            p.SaveChanges();
        }

        private void UpdateActivityDates(string username, bool isAuthenticated)
        {
            UserProfile profile = GetProfile(username, isAuthenticated);

            //only update the database if this is the first activity in over 10 minute
            if (profile.LastUpdatedDate.AddMinutes(10) < DateTime.Now.ToUniversalTime()
                || profile.LastActivityDate.AddMinutes(10) < DateTime.Now.ToUniversalTime())
            {
                profile.LastActivityDate = DateTime.Now.ToUniversalTime();
                profile.LastUpdatedDate = DateTime.Now.ToUniversalTime();
                _db.SaveChanges(profile);
            }
        }

        #endregion

        public override void Initialize(string name, NameValueCollection config)
        {
            //
            // Initialize values from web.config.
            //
            if (config == null)
                throw new ArgumentNullException("config");

            if (string.IsNullOrEmpty(name))
                name = "SdbProfileProvider";

            if (string.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "ScrappyDB ProfileProvider");
            }


            if (string.IsNullOrEmpty(config["applicationName"]))
            {
                if (!string.IsNullOrEmpty(HostingEnvironment.ApplicationVirtualPath))
                {
                    _applicationName = HostingEnvironment.ApplicationVirtualPath;
                }
                else
                {
                    _applicationName = "SdbProfileProvider";
                }
            }
            else
            {
                _applicationName = config["applicationName"];
            }

            config["applicationName"] = _applicationName;

            // Initialize the abstract base class.
            base.Initialize(name, config);

            //
            // Initialize SimpleDB.
            //
            Cache cache = null;
            if (HttpContext.Current != null)
                cache = HttpContext.Current.Cache;
            _db = new Db(cache);

            _common = new MembershipCommon(_db);
        }
    }
}